package ul.audio

import javax.sound.{sampled};
import java.util.{Timer, TimerTask};
import java.util.concurrent.{atomic};
import scala.{actors};
import scala.{math};
import scala.util.matching.{Regex};
import scala.collection.{mutable};


case class  SetMixer( mixer: sampled.Mixer );
case class  SetSourceLine( line: sampled.SourceDataLine);
case class  SetGenerator( generator: FunctionGenerator );
case object StartGeneration;
case object StopGeneration;


/** functional generator - generates consecutive periodic function values for current angle (0..2*Pi),
 *  adjusts angle value by angle step
 *  @param angleInit - initial angle value (0..2*Pi)
 *  @param angleStepInit - angle step
 *  @param amplitudeInit - function half-amplitude (32000.0 for signal with -32000..32000 range)
 *  @param t - localization translation function
 */
abstract class FunctionGenerator(angleStepInit:Double, amplitudeInit:Double, angleInit:Double) {
    val PiD2 = math.Pi / 2;
    val PiM2 = math.Pi * 2;
    var angle = angleInit; // current angle
    var angleStep = angleStepInit; // current angle step
    var amplitude = amplitudeInit; // current function half-amplitude
    
    def this() = this(math.Pi / 180.0, 1.0, 0.0);
    // constructor with function/sample frequency parameters
    def this( funFreq:Double, sampleFreq:Double, amplitudeInit:Double, angleInit:Double ) =
        this( 2 * math.Pi * funFreq / sampleFreq, amplitudeInit, angleInit );
    
    // calculates angle step from discretization frequency and function frequency
    def setParams( angleStepInit:Double, amplitudeInit:Double, angleInit:Double ) = {
        angleStep = angleStepInit;
        angle = angleInit;
        amplitude = amplitudeInit;
    }
    // calculates angle step from discretization frequency and function frequency
    def setParams( funFreq:Double, sampleFreq:Double, amplitudeInit:Double, angleInit:Double ) = {
        angleStep = 2 * math.Pi * funFreq / sampleFreq;
        angle = angleInit;
        amplitude = amplitudeInit;
    }
    // returns function value for current angle and adjusts angle to next value
    def nextSample:Double = {
        val result = getSample * amplitude;
        angle += angleStep;
        while (angle > PiM2) angle -= PiM2;
        result;
    }
    // returns normalized function value (in range -1.0..1.0) for current angle
    def getSample: Double;
}
// sine generator
class SineGenerator( angleStepInit:Double, amplitudeInit:Double, angleInit:Double )
        extends FunctionGenerator( angleStepInit, amplitudeInit, angleInit ) {
    override def getSample: Double = math.sin(angle);
}
// cosine generator
class CosineGenerator( angleStepInit: Double, amplitudeInit: Double, angleInit: Double )
        extends FunctionGenerator( angleStepInit, amplitudeInit, angleInit ) {
    override def getSample: Double = math.cos(angle);
}
// triangle generator
class TriangleGenerator( angleStepInit: Double, amplitudeInit: Double, angleInit: Double )
        extends FunctionGenerator( angleStepInit, amplitudeInit, angleInit ) {
    override def getSample:Double = {
        if (angle <= PiD2) (2 * angle / math.Pi)
        else if (angle <= (math.Pi * 1.5)) (2 * (1 - (angle / math.Pi)))
        else (2 * ((angle / math.Pi) - 2));
    }
}
// saw generator
class SawGenerator( angleStepInit: Double, amplitudeInit: Double, angleInit: Double )
        extends FunctionGenerator( angleStepInit, amplitudeInit, angleInit ) {
    override def getSample: Double = ((math.Pi - angle) / math.Pi);
}
// inverted saw generator
class InvSawGenerator( angleStepInit: Double, amplitudeInit: Double, angleInit: Double )
        extends FunctionGenerator( angleStepInit, amplitudeInit, angleInit ) {
    override def getSample: Double = ((angle - math.Pi) / math.Pi);
}
// square generator
class SquareGenerator( angleStepInit: Double, amplitudeInit: Double, angleInit: Double )
        extends FunctionGenerator( angleStepInit, amplitudeInit, angleInit ) {
    override def getSample: Double = if (angle < math.Pi) -1.0 else 1.0;
}
// table-defined generator
class TableGenerator( angleStepInit: Double, amplitudeInit: Double, angleInit: Double, samples: Seq[Double] )
        extends FunctionGenerator( angleStepInit, amplitudeInit, angleInit ) {
    val samplesTable = samples.toArray;

    override def getSample: Double = 1.0;
}


class Generator extends actors.Actor {

    var started = new atomic.AtomicBoolean(false);
    var mixer: sampled.Mixer = null; // current mixer
    var sourceLine: sampled.SourceDataLine = null; // current source line
    var generator: FunctionGenerator = null;
    var genTimer: Timer = null;
    val genBuffer = new mutable.ArrayBuffer[Byte];

    def act() {
        while (true) {
            receive {
                case SetMixer( mixer: sampled.Mixer ) =>
                    if (!this.started.get) this.mixer = mixer;
                case SetSourceLine( line: sampled.SourceDataLine) =>
                    if (!this.started.get) this.sourceLine = line;
                case SetGenerator( generator: FunctionGenerator ) =>
                    if (!this.started.get) this.generator = generator;
                case StartGeneration =>
                    if (!this.started.get) {
                        sourceLine.start;
                        genTimer = new Timer {
                            scheduleAtFixedRate( new TimerTask {
                                override def run = {
                                    var freeBytes = sourceLine.available & 0xFFFFFFFE;
                                    if (freeBytes > 8) {
                                        genBuffer.clear;
                                        for (sample <- 0 until (freeBytes >> 1)) {
                                            val nextSample = generator.nextSample.toShort;
                                            genBuffer += (nextSample & 0xFF).toByte;
                                            genBuffer += ((nextSample >> 8) & 0xFF).toByte;
                                        }
                                        sourceLine.write( genBuffer.toArray, 0, freeBytes );
                                    }
                                }},
                                0,
                                10
                            )
                        };
                        this.started.set( true );
                    }
                case StopGeneration =>
                    if (this.started.get) {
                        sourceLine.stop;
                        sourceLine.close;
                        genTimer.cancel;
                        this.started.set( false );
                    }
                case _ =>
            }
        }
    }
}



object Audio {

    // returns list of mixers info
    def mixers: Seq[sampled.Mixer.Info] = {
        this.synchronized {
            return sampled.AudioSystem.getMixerInfo;
        }
    }
    def mixersNames: Seq[String] = this.mixers.map( (mi) => mi.toString );
    // returns mixer by info
    def mixer( info: sampled.Mixer.Info ): Option[sampled.Mixer] = {
        this.synchronized {
            return Some(sampled.AudioSystem.getMixer( info ));
            //} catch { case _ => return None }
        }
    }
    // returns mixer by name
    def mixer( name: String ): Option[sampled.Mixer] = {
        this.synchronized {
            val m = mixers.find( (m) => (m.toString == name) );
            if (m.isEmpty) None else this.mixer(m.get);
        }
    }
    // returns mixer by name Regex
    def mixer( nameRe: Regex ): Option[sampled.Mixer] = {
        this.synchronized {
            val m = mixers.find( (m) => (!(nameRe.findFirstIn(m.toString).isEmpty)) );
            if (m.isEmpty) None else this.mixer(m.get);
        }
    }

    // returns target lines for mixer
    def targetLines( info: sampled.Mixer.Info ): Seq[sampled.Line] = {
        this.synchronized {
            val m = this.mixer(info);
            return if (m.isEmpty) Nil else m.get.getTargetLineInfo.map( (tli) => sampled.AudioSystem.getLine(tli) );
        }
    }
    def targetLines( mixer: sampled.Mixer ): Seq[sampled.Line] = targetLines(mixer.getMixerInfo);
    def targetDataLines( mixer: sampled.Mixer ): Seq[sampled.TargetDataLine] = targetLines(mixer).filter( (tl) => tl.isInstanceOf[sampled.TargetDataLine] ).map((tl) => tl.asInstanceOf[sampled.TargetDataLine]);
    def targetClips( mixer: sampled.Mixer ): Seq[sampled.Clip] = targetLines(mixer).filter( (tl) => tl.isInstanceOf[sampled.Clip] ).map((tl) => tl.asInstanceOf[sampled.Clip]);
    def targetPorts( mixer: sampled.Mixer ): Seq[sampled.Port] = targetLines(mixer).filter( (tl) => tl.isInstanceOf[sampled.Port] ).map((tl) => tl.asInstanceOf[sampled.Port]);

    // returns source lines for mixer
    def sourceLines( info: sampled.Mixer.Info ): Seq[sampled.Line] = {
        this.synchronized {
            val m = this.mixer(info);
            return if (m.isEmpty) Nil else m.get.getSourceLineInfo.map( (sli) => sampled.AudioSystem.getLine(sli) );
        }
    }
    def sourceLines( mixer: sampled.Mixer ): Seq[sampled.Line] = sourceLines(mixer.getMixerInfo);
    def sourceDataLines( mixer: sampled.Mixer ): Seq[sampled.SourceDataLine] = sourceLines(mixer).filter( (sl) => sl.isInstanceOf[sampled.SourceDataLine] ).map((tl) => tl.asInstanceOf[sampled.SourceDataLine]);
    def sourceClips( mixer: sampled.Mixer ): Seq[sampled.Clip] = sourceLines(mixer).filter( (sl) => sl.isInstanceOf[sampled.Clip] ).map((tl) => tl.asInstanceOf[sampled.Clip]);
    def sourcePorts( mixer: sampled.Mixer ): Seq[sampled.Port] = sourceLines(mixer).filter( (sl) => sl.isInstanceOf[sampled.Port] ).map((tl) => tl.asInstanceOf[sampled.Port]);

    // finds source line by its name
    def sourceDataLine( mixer: sampled.Mixer, name: String ): Option[sampled.SourceDataLine] = {
        this.synchronized {
            val sl = sourceDataLines(mixer).find( (sl) => (sl.getLineInfo.toString == name ));
            return if (sl.isEmpty) None else Some(sl.get.asInstanceOf[sampled.SourceDataLine]);
        }
    }
    // finds source line by name Regex
    def sourceDataLine( mixer: sampled.Mixer, nameRe: Regex ): Option[sampled.SourceDataLine] = {
        this.synchronized {
            val sl = mixer.getSourceLines.find( (sl) => (!(nameRe.findFirstIn(sl.getLineInfo.toString).isEmpty)) );
            return if (sl.isEmpty) None else Some(sl.get.asInstanceOf[sampled.SourceDataLine]);
        }
    }

}

